// CoreCalc, a spreadsheet core implementation 

// ----------------------------------------------------------------------
// Copyright (c) 2006-2009 Thomas S. Iversen and Peter Sestoft

// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use, copy,
// modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:

//  * The above copyright notice and this permission notice shall be
//    included in all copies or substantial portions of the Software.

//  * The software is provided "as is", without warranty of any kind,
//    express or implied, including but not limited to the warranties of
//    merchantability, fitness for a particular purpose and
//    noninfringement.  In no event shall the authors or copyright
//    holders be liable for any claim, damages or other liability,
//    whether in an action of contract, tort or otherwise, arising from,
//    out of or in connection with the software or the use or other
//    dealings in the software.
// ----------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Diagnostics;            // Stopwatch
using System.Globalization;          // CultureInfo
using System.IO;			               // MemoryStream, Stream
using System.Text;
using System.Xml;
using System.Windows.Forms;
using ICSharpCode.SharpZipLib.GZip;  // Gnumeric format is compressed

// TODO: Replace use of Scanner and Parser with Cell.Parse

// IOFormat is used to read XML files containing spreadsheets.
// Currently, Excel 2003 XMLSS format and Gnumeric format are supported.

namespace CoreCalc {
  abstract class IOFormat {
    public abstract Workbook Read(String filename);
    private String fileExtension;
    private String description;

    public static Stream MakeStream(String s) {
      char[] cs = s.ToCharArray();
      byte[] bs = new byte[cs.Length];
      for (int i = 0; i < cs.Length; i++)
        bs[i] = (byte)(cs[i]);
      return new MemoryStream(bs);
    }

    public String GetFilter() {
      return description + " (*." + fileExtension + ")|*." + fileExtension;
    }

    public IOFormat(String fileextension, String description) {
      this.fileExtension = fileextension;
      this.description = description;
    }

    public Boolean ValidExtension(String ext) {
      return this.fileExtension == ext;
    }
  }

  // ----------------------------------------

  // Read and write Gnumeric format XML files

  sealed class GnumericIOFormat : IOFormat {
    const int MINROWS = 10, MINCOLS = 10;

    public GnumericIOFormat()
      : base("gnumeric", "Gnumeric format")
    {
    }

    private Boolean isGnumeric(XmlDocument xmldoc) {
      XmlElement docroot = xmldoc.DocumentElement;
      return (docroot.Name == "gnm:Workbook");
    }

    private Workbook ParseGnumeric(XmlDocument xmldoc) {
      Workbook wb = null;
      Scanner scanner;
      Parser parser;
      Cell cell;

      int i, row, col, valuetype, value, exprid, rows, cols;
      String innertext;

      // Is this a gnumeric spreadsheet?
      if (isGnumeric(xmldoc)) {
        wb = new Workbook();
        XmlNodeList Xmlsheets = xmldoc.GetElementsByTagName("gnm:Sheet");
        XmlNamespaceManager xmlnsm = new XmlNamespaceManager(xmldoc.NameTable);
        xmlnsm.AddNamespace("gnm", "http://www.gnumeric.org/v10.dtd");
        XmlNode Xmlsheet, sheetnamenode, maxcolnode, maxrownode;
        IDictionary<int, Expr> exprids = new Dictionary<int, Expr>();

        String sheetname;
        int maxcol, maxrow;
        for (i = 0; i < Xmlsheets.Count; i++) {
          Xmlsheet = Xmlsheets[i];
          sheetnamenode = Xmlsheet.SelectSingleNode("gnm:Name", xmlnsm);
          maxcolnode = Xmlsheet.SelectSingleNode("gnm:MaxCol", xmlnsm);
          maxrownode = Xmlsheet.SelectSingleNode("gnm:MaxRow", xmlnsm);

          if (sheetnamenode != null && maxcolnode != null && maxrownode != null) {
            sheetname = sheetnamenode.InnerText;
            // Gnumeric maxrow and maxcol are base 0, ours are base 1:
            try { maxcol = Math.Max(int.Parse(maxcolnode.InnerText) + 1, MINCOLS); }
            catch (Exception) { maxcol = MINCOLS; }
            try { maxrow = Math.Max(int.Parse(maxrownode.InnerText)+1, MINROWS); }
            catch (Exception) { maxrow = MINROWS; }

            // Add Sheet
            new Sheet(wb, sheetname, maxcol, maxrow, false);

            //Console.WriteLine("{0} {1} {2}", sheetname, maxrow, maxcol);
          }
          else {
            // If we can't parse values, bail out.
            return null;
          }
        }

        // The sheets are in place; now fill in values. 
        // Iterate over sheets in wb instead of XmlNodeList sheets

        i = 0;
        foreach (Sheet sheet in wb) {
          Xmlsheet = Xmlsheets[i];
          XmlNodeList Xmlcells = Xmlsheet.SelectNodes("gnm:Cells/gnm:Cell", xmlnsm);

          XmlNode Xmlcell;
          XmlAttribute xmla;

          // Iterate over all cell definitions in this Sheet.
          for (int j = 0; j < Xmlcells.Count; j++) {
            Xmlcell = Xmlcells[j];
            XmlAttributeCollection xmlac = Xmlcell.Attributes;

            row = col = exprid = valuetype = rows = cols = -1;

            // Foreach cell, find its attributes.
            for (int k = 0; k < xmlac.Count; k++) {
              xmla = xmlac[k];

              if (xmla != null) {
                try { value = int.Parse(xmla.Value); }
                catch { value = -1; }

                switch (xmla.Name) {
                  case "Row":
                    row = value;
                    break;
                  case "Col":
                    col = value;
                    break;
                  case "ValueType":
                    valuetype = value;
                    break;
                  case "ExprID":
                    exprid = value;
                    break;
                  case "Rows":
                    rows = value;
                    break;
                  case "Cols":
                    cols = value;
                    break;
                  default:
                    // Other Names do exist. For instance ValueFormat d/m/yy. 
                    // But they are not support as of now.
                    break;
                }
              }
            }

            if (row != -1 && col != -1) {
              innertext = Xmlcell.InnerText;

              // valuetype=60 => string
              if (valuetype == 60) {
                innertext = "'" + innertext;
              }

              // If innertext is missing and exprid is set, 
              // It is a cached formula
              if ((innertext == null | innertext.Equals("")) && exprid != -1) {
                cell = new Formula(wb, exprids[exprid]);
              }
              else {
                scanner = new Scanner(MakeStream(innertext));
                parser = new Parser(scanner);
                cell = parser.ParseCell(wb, col, row);

                Formula f = cell as Formula;
                if (exprid != -1 && f != null) {
                  exprids[exprid] = f.Expr;
                }
              }
              if (rows != -1 || cols != -1) {
                // Area
                CellAddr c1 = new CellAddr(col, row);
                CellAddr c2 = new CellAddr(col + cols - 1, row + rows - 1);

                sheet.InsertArrayFormula(cell, col, row, c1, c2);
              }
              else {
                sheet[col, row] = cell;
              }
            }
          }
          i++;
        }
      }
      return wb;
    }

    public override Workbook Read(String filename) {
      Workbook wb;
      long membefore = System.GC.GetTotalMemory(true);

      Stopwatch watch = new Stopwatch();
      watch.Reset(); watch.Start();

      Stream s = new GZipInputStream(File.OpenRead(filename));
      XmlDocument xmldoc = new XmlDocument();
      xmldoc.Load(s);
      long memafter = System.GC.GetTotalMemory(true);
      wb = ParseGnumeric(xmldoc);
      watch.Stop();
      Console.WriteLine("Loaded gnumeric spreadsheet in {0} ms", watch.ElapsedMilliseconds.ToString());
      Console.WriteLine("Memory before: {0}\nMemory After: {1}\nMemory Consumed: {2}", membefore, memafter, memafter - membefore);
      return wb;
    }
  }

  // --------------------------------------------------

  // Read and write XMLSS (Excel 2003 XML format) files

  sealed class XMLSSIOFormat : IOFormat {
    const int MINROWS = 10, MINCOLS = 10;

    readonly Formats fo = new Formats();
    IDictionary<String, Expr> expressionindex;

    public XMLSSIOFormat()
      : base("xml", "XMLSS") {
      fo.RefFmt = Formats.RefType.R1C1;

      // Used for finding identical formulas: same R1C1 representation
      expressionindex = new Dictionary<String, Expr>();
    }

    private void ParseRow(XmlReader rowreader, Workbook wb, Sheet sheet, int row) {
      /* XMLSS has origo at (1,1) = (A,1)
       * CoreCalc internal representation has origo at (0,0) = (A,1)
       */

      int col = 0;
      int colindex = 0;
      XmlReader cellreader = rowreader.ReadSubtree();
      Scanner scanner;
      Parser parser;
      while (cellreader.ReadToFollowing("Cell")) {
        String colindexstr = cellreader.GetAttribute("ss:Index");
        String arrayrangestr = cellreader.GetAttribute("ss:ArrayRange");
        String formulastr = cellreader.GetAttribute("ss:Formula");
        String typestr = "";
        String dataval = "";

        if (colindexstr != null) {
          try { colindex = int.Parse(colindexstr); }
          catch { colindex = 0; }
          col = colindex;
        }
        else {
          col++;
        }

        XmlReader datareader = cellreader.ReadSubtree();
        if (datareader.ReadToFollowing("Data")) {
          typestr = datareader.GetAttribute("ss:Type");
          datareader.MoveToContent();
          dataval = datareader.ReadElementContentAsString();
        }

        if (formulastr != null) {
          scanner = new Scanner(MakeStream(formulastr));
          //Console.WriteLine("Trying to parse formula: {0}", formulastr);
        }
        else {
          // Anything else than formulas are values.
          // If XMLSS tells us it is a string we believe it to be so.

          if (typestr == "String")
            dataval = "'" + dataval;

          scanner = new Scanner(MakeStream(dataval));
          //Console.WriteLine("Trying to parse datavalue: {0}", dataval);
        }

        // If an array result occupies cells, do not overwrite
        // the formula with precomputed and cached data from 
        // the XMLSS file. Instead skip the parsing and sheetupdate.
        Cell existingcell = sheet[col - 1, row - 1];
        if (existingcell != null)
          continue;

        parser = new Parser(scanner);
        parser.SetNumberDecimalSeparator(".");
        Cell cell = parser.ParseCell(wb, col, row);
        //Cell cell = null;

        if (arrayrangestr != null && cell is Formula) { // Array formula
          string[] split = arrayrangestr.Split(":".ToCharArray());

          RARef raref1 = new RARef(split[0]);
          RARef raref2;
          if (split.Length == 1) {
            // FIXME: single cell result, but still array
            raref2 = new RARef(split[0]);
          }
          else {
            raref2 = new RARef(split[1]);
          }

          if (raref1 != null && raref2 != null) {
            CellAddr celladdr1 = raref1.Addr(col - 1, row - 1);
            CellAddr celladdr2 = raref2.Addr(col - 1, row - 1);
            //Console.WriteLine("({0},{1}), ({2},{3})", celladdr1.col, celladdr1.row, celladdr2.col, celladdr2.row);

            sheet.InsertArrayFormula(cell, col - 1, row - 1, celladdr1, celladdr2);
          }
        }
        else {   // One-cell formula, or constant
          Formula f = cell as Formula;
          if (f != null) {
            String formulaexpression = f.Show(col, row, fo);
            if (expressionindex.ContainsKey(formulaexpression))
              f.Expr = expressionindex[formulaexpression];
            else
              expressionindex[formulaexpression] = f.Expr;

            //Console.WriteLine("{0}", cell.Show(col, row, fo));
          }
          sheet[col - 1, row - 1] = cell;
        }
        datareader.Close();
      }
      cellreader.Close();
    }

    private void ParseSheet(XmlReader sheetreader, Workbook wb, Sheet sheet) {
      int row;  // current row
      if (sheetreader.ReadToFollowing("Table")) {
        row = 0;
        XmlReader rowreader = sheetreader.ReadSubtree();
        while (rowreader.ReadToFollowing("Row")) {
          String rowindexstr = rowreader.GetAttribute("ss:Index");
          int rowindex;

          if (rowindexstr != null) {
            try { rowindex = int.Parse(rowindexstr); }
            catch { rowindex = 0; }
            row = rowindex;
          }
          else {
            row++;
          }
          ParseRow(rowreader, wb, sheet, row);
        }
        rowreader.Close();
      }
    }

    private void ParseSheets(XmlTextReader reader, Workbook wb) {
      while (reader.ReadToFollowing("Worksheet")) {
        String sheetname = reader.GetAttribute("ss:Name");
        XmlReader sheetreader = reader.ReadSubtree();
        Sheet sheet = wb[sheetname];
        ParseSheet(sheetreader, wb, sheet);
        ScanSheet(sheet, RegisterSdfs);
        ScanSheet(sheet, CreateSdfs);
        sheetreader.Close();
      }
      wb.AddCellChangeListeners();
    }

    private void ScanSheet(Sheet sheet, Act<Sheet, int, int> f)
    {
      if (sheet.IsFunctionSheet)
        for (int row = 0; row < sheet.Rows; row++)
          for (int col = 0; col < sheet.Cols; col++)
            f(sheet, col, row);
    }

    // Register SDFs (and maybe later: convert DELAY cell calls to DelayCell)
    private void RegisterSdfs(Sheet sheet, int col, int row)
    {
      Cell cell = sheet[col, row];
      if (cell == null || !(cell is Formula))
        return;
      Expr e = (cell as Formula).Expr;
      if (!(e is FunCall))
        return;
      FunCall funCall = e as FunCall;
      Expr[] es = funCall.es;
      switch (funCall.function.name)
      {
        case "MAKEFUN":
          if (es.Length >= 2 && es[0] is TextConst && es[1] is CellRef)
          {
            String sdfName = (es[0] as TextConst).value.value;
            FullCellAddr outputCell = (es[1] as CellRef).GetAbsoluteAddr(sheet, col, row);
            FullCellAddr[] inputCells = new FullCellAddr[es.Length - 2];
            bool ok = true;
            for (int i = 2; ok && i < es.Length; i++)
            {
              Expr inputCellRef = es[i];
                  ok = inputCellRef is CellRef;
              if (ok)
                inputCells[i - 2] = (inputCellRef as CellRef).GetAbsoluteAddr(sheet, col, row);
            }
            if (ok)
              SheetDefinedFunctions.SdfManager.Register(outputCell, inputCells, sdfName);
          }
          break;
        case "DELAY":
          break;
        default:
          /* do nothing */
          break;
      }
    }

    // Create SDFs 
    private void CreateSdfs(Sheet sheet, int col, int row)
    {
      Cell cell = sheet[col, row];
      if (cell == null || !(cell is Formula))
        return;
      Expr e = (cell as Formula).Expr;
      if (!(e is FunCall))
        return;
      FunCall funCall = e as FunCall;
      Expr[] es = funCall.es;
      switch (funCall.function.name)
      {
        case "MAKEFUN":
          if (es.Length >= 2 && es[0] is TextConst && es[1] is CellRef)
          {
            String sdfName = (es[0] as TextConst).value.value;
            FullCellAddr outputCell = (es[1] as CellRef).GetAbsoluteAddr(sheet, col, row);
            FullCellAddr[] inputCells = new FullCellAddr[es.Length - 2];
            bool ok = true;
            for (int i = 2; ok && i < es.Length; i++)
            {
              Expr inputCellRef = es[i];
              ok = inputCellRef is CellRef;
              if (ok)
                inputCells[i - 2] = (inputCellRef as CellRef).GetAbsoluteAddr(sheet, col, row);
            }
            if (ok)
              SheetDefinedFunctions.SdfManager.CreateFunction(sdfName, outputCell, inputCells);
          }
          break;
        default:
          /* do nothing */
          break;
      }
    }

    private Boolean isXMLSS(XmlTextReader reader) {
      return reader.NodeType == XmlNodeType.Element &&
             reader.Name == "Workbook" &&
             reader.GetAttribute("xmlns") != null;
    }

    private void ParseWorkBook(XmlTextReader reader, Workbook wb) {
      while (reader.Read()) {
        if (isXMLSS(reader)) {
          ParseSheets(reader, wb);
        }
      }
    }

    private Workbook EmptyWorkbook(XmlTextReader reader) {
      int cols = 0; // number of columns in sheet (parsed)
      int rows = 0; // number of rows in sheet (parsed)
      Workbook wb = null;
      Sheet sheet = null;
      String tmpstr;
      while (reader.Read()) {
        if (isXMLSS(reader)) {
          wb = new Workbook();
          String sheetname;
          while (reader.ReadToFollowing("Worksheet")) {
            sheetname = reader.GetAttribute("ss:Name");
            XmlReader sheetreader = reader.ReadSubtree();
            bool functionSheet = sheetname.StartsWith("@");
            if (sheetreader.ReadToFollowing("Table")) {
              // Assumption: ExpandedRowCount and ExpandedColumnCount exists
              // EVEN THOUGH not required.

              tmpstr = sheetreader.GetAttribute("ss:ExpandedRowCount");
              try { rows = Math.Max(int.Parse(tmpstr), MINROWS); }
              catch (Exception) { rows = MINROWS; }
              tmpstr = sheetreader.GetAttribute("ss:ExpandedColumnCount");
              try { 
                cols = Math.Max(int.Parse(tmpstr), MINCOLS); 
              } catch (Exception) { cols = MINCOLS; }
              sheet = new Sheet(wb, sheetname, cols, rows, functionSheet);
            }
            else {
              // If no table exists, then the sheet does exist but is empty.
              sheet = new Sheet(wb, sheetname, MINCOLS, MINROWS, functionSheet);
            }
            sheetreader.Close();
          }
        }
      }
      return wb;
    }

    public override Workbook Read(String filename) {
      XmlTextReader reader = null;
      Workbook wb = null;
      Stopwatch watch = new Stopwatch();

      watch.Reset(); watch.Start();
      try {
        reader = new XmlTextReader(filename);
        reader.WhitespaceHandling = WhitespaceHandling.None;
        wb = EmptyWorkbook(reader);
        if (reader != null)
          reader.Close();
        if (wb != null) {
          reader = new XmlTextReader(filename);
          reader.WhitespaceHandling = WhitespaceHandling.None;
          try {
            ParseWorkBook(reader, wb);
          }
          catch (BadFormatException str) {
            DialogResult result = MessageBox.Show("Bad Format: " + str,
                                  "Bad Format",
                                  MessageBoxButtons.OK,
                                  MessageBoxIcon.Error);
            return null;
          }
        }
      }
      finally {
        if (reader != null)
          reader.Close();
      }
      watch.Stop();
      Console.WriteLine("Loaded xmlss spreadsheet in {0} ms",
                        watch.ElapsedMilliseconds.ToString());
      return wb;
    }
  }

  public class WorkBookIO {
    private List<IOFormat> formats;
    private IOFormat defaultformat;
    public WorkBookIO() {
      formats = new List<IOFormat>();
      AddFormat(new XMLSSIOFormat(), true);
      AddFormat(new GnumericIOFormat(), false);
    }

    private void AddFormat(IOFormat format, bool def) {
      formats.Add(format);
      if (def)
        defaultformat = format;
    }

    private IOFormat FindFormat(String filename) {
      String[] fields = filename.Split((".").ToCharArray());
      String ext = fields[fields.Length - 1];
      foreach (IOFormat format in formats) {
        if (format.ValidExtension(ext))
          return format;
      }
      return null;
    }

    // Attempt to read a files using a supported format.
    // The algorithm is as follows: First we try a supported format using
    // a filename extension. If no method for that extension is found, or if the
    // method found raises an exception, this method tries the supported formats 
    // one by one. If that does not succeed either, a null value is returned.

    public Workbook Read(String filename) {
      Workbook wb;
      IOFormat format = FindFormat(filename);

      // If we found one, try that format
      if (format != null) {
        wb = format.Read(filename);
        if (wb != null)
          return wb;
      }

      // Else try them all.
      foreach (IOFormat format1 in formats) {
        wb = format1.Read(filename);
        if (wb != null)
          return wb;
      }

      // give up.
      return null;
    }

    public String SupportedFormatFilter() {
      StringBuilder sb = new StringBuilder();
      foreach (IOFormat ioformat in formats) {
        sb.Append(ioformat.GetFilter());
        sb.Append("|");
      }

      sb.Append("All files (*.*)|*.*");
      return sb.ToString();
    }

    public int DefaultFormatIndex() {
      return formats.IndexOf(defaultformat) + 1;
    }
  }
}
